<template>
  <div>
    <v-row no-gutters>
      <!-- Left Palette -->
      <v-col cols="12" md="2" class="pr-2">
        <v-card outlined>
          <v-divider></v-divider>
          <div class="palette" :style="{ maxHeight: canvasHeight + 'px', overflowY: 'auto' }">
            <template v-for="(group, gi) in paletteGroups">
              <div :key="'g-'+gi" class="palette-group-title">{{ group.title }}</div>
              <div
                class="palette-item"
                v-for="p in group.items"
                :key="group.title + '-' + p.type"
                draggable="true"
                @dragstart="onPaletteDragStart($event, p)"
              >
                <v-avatar size="28" class="mr-2" :style="{ background: p.bg }"><span v-html="p.icon" /></v-avatar>
                <div>
                  <div class="subtitle-2 font-weight-bold">{{ p.label }}</div>
                  <div class="caption grey--text">{{ p.desc }}</div>
                </div>
              </div>
              <v-divider :key="'d-'+gi" class="my-1" v-if="gi < paletteGroups.length - 1"></v-divider>
            </template>
          </div>
        </v-card>
      </v-col>

      <!-- Canvas Area -->
      <v-col cols="12" md="10" lg="10" xl="10" class="pr-0 pl-2">
        <div
          class="canvas-wrap"
          :class="{ 'grabbable': true, 'grabbing': isPanning }"
          :style="{ height: canvasHeight + 'px' }"
          ref="canvasWrap"
          @mousedown.self="onCanvasMouseDown"
          @mousemove="onCanvasMouseMove"
          @mouseup="onCanvasMouseUp"
          @contextmenu.prevent
          @dragover.prevent
          @drop="onCanvasDrop"
        >
          <!-- Top-right toolbar: diagram select + Save button -->
          <div class="canvas-toolbar">
            <v-select
              dense
              hide-details
              outlined
              class="mr-2 toolbar-select"
              :items="diagramOptions"
              item-text="name"
              item-value="id"
              :loading="loadingDiagrams"
              v-model="diagramId"
              @change="onDiagramChange"
              placeholder="Select diagram"
            />
            <v-btn small :height="40" class="mr-2" @click.stop="undo" :disabled="!canUndo()">Undo</v-btn>
            <v-btn small :height="40" class="mr-2" color="error" @click.stop="onDelete" :disabled="!selectedNodeId && !selectedEdgeId">Delete</v-btn>
            <v-btn small :height="40" color="primary" :loading="savingGraph" @click.stop="saveCurrentGraph">Save</v-btn>
          </div>
          
          <!-- Zoom controls -->
          <div class="zoom-controls">
            <v-btn small icon @click="zoomIn" class="zoom-btn">
              <v-icon>mdi-magnify-plus</v-icon>
            </v-btn>
            <v-btn small icon @click="zoomOut" class="zoom-btn">
              <v-icon>mdi-magnify-minus</v-icon>
            </v-btn>
            <v-btn small icon @click="resetZoom" class="zoom-btn">
              <v-icon>mdi-magnify-close</v-icon>
            </v-btn>
            <div class="zoom-level">{{ Math.round(zoom * 100) }}%</div>
          </div>
          <svg :width="canvasWidth" :height="canvasHeight" @mousedown.self="onCanvasMouseDown" @wheel="onWheel">
            <g :transform="`translate(${panX}, ${panY}) scale(${zoom})`">
            <!-- Edges -->
            <g>
              <path
                v-for="edge in edges"
                :key="edge.id"
                :d="getEdgePath(edge)"
                :stroke="isEdgeSelected(edge) ? '#3949ab' : '#90a4ae'"
                :stroke-width="isEdgeSelected(edge) ? 3 : 2"
                fill="none"
                marker-end="url(#arrow)"
                class="edge-path clickable-edge"
                @click.stop="onEdgeClick(edge)"
              />
              <!-- Edge labels (e.g., power) -->
              <text
                v-for="edge in edges"
                :key="edge.id + '-label'"
                :x="getEdgeLabelPos(edge).x"
                :y="getEdgeLabelPos(edge).y"
                class="edge-label"
                font-size="12"
                fill="#455a64"
              >
                {{ formatEdgeLabel(edge) }}
              </text>
              <path
                v-if="connecting && tempEdge"
                :d="getElbowPath(tempEdge.x1, tempEdge.y1, tempEdge.x2, tempEdge.y2)"
                stroke="#9fa8da"
                stroke-width="2"
                stroke-dasharray="4 4"
                fill="none"
                marker-end="url(#arrow)"
              />
            </g>

            <!-- Nodes -->
            <g>
              <g
                v-for="node in nodes"
                :key="node.id"
                class="node"
                :transform="`translate(${node.x}, ${node.y})`"
                @mousedown.prevent.stop="onNodeMouseDown(node, $event)"
                @mouseup.stop="onNodeMouseUp(node)"
                @dblclick.stop="openProperties(node)"
                @contextmenu.prevent.stop="openProperties(node)"
                @mouseover="hoverNodeId = node.id"
                @mouseleave="hoverNodeId = null"
              >
                <rect :x="-nodeWidth/2" :y="-nodeHeight/2" :width="nodeWidth" :height="nodeHeight" rx="10" ry="10"
                      :fill="'#ffffff'" :stroke="isSelected(node) ? '#3949ab' : (hoverNodeId === node.id ? '#3f51b5' : '#b0bec5')" stroke-width="2" />
                <rect :x="-nodeWidth/2+2" :y="-nodeHeight/2+2" :width="nodeWidth-4" :height="nodeHeight-4" rx="8" ry="8" :fill="getNodeFill(node)" opacity="0.05" />
                <text :x="-nodeWidth/2 + 36" dy="4" font-size="14" fill="#263238">{{ node.label }}</text>
                <text :x="-nodeWidth/2 + 12" dy="5" font-size="16">{{ getNodeIcon(node) }}</text>
                <!-- Connector handle (right side) -->
                <circle class="connector-handle" :cx="nodeWidth/2 + 10" cy="0" r="6" fill="#fff" stroke="#3f51b5" stroke-width="2" @mousedown.stop="onConnectorMouseDown(node, $event)" />
              </g>
            </g>

            <!-- Arrow marker -->
            <defs>
              <marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto">
                <path d="M 0 0 L 10 5 L 0 10 z" fill="#90a4ae" />
              </marker>
            </defs>
            </g>
          </svg>
        </div>
      </v-col>

      <!-- Properties Panel (floating, non-modal) -->
      <div v-show="propDrawer" class="prop-panel" :style="{ top: (appBarH + 96) + 'px', bottom: '96px' }">
        <v-card outlined>
          <v-toolbar flat dense>
            <v-toolbar-title>Component Properties</v-toolbar-title>
            <v-spacer></v-spacer>
            <v-btn icon @click="onCancelProp"><v-icon>mdi-close</v-icon></v-btn>
          </v-toolbar>
          <v-divider></v-divider>
          <v-card-text v-if="formModel">
            <v-row dense>
              <v-col cols="12" sm="6">
                <v-text-field v-model="formModel.label" label="Name" outlined dense />
              </v-col>
              <v-col cols="12" sm="6">
                <v-text-field :value="formModel.id" label="Component ID" outlined dense disabled />
              </v-col>
              <v-col cols="12" sm="6">
                <v-select v-model="formModel.type" :items="typeOptions" label="Type" outlined dense />
              </v-col>
              <v-col cols="12" sm="6">
                <v-text-field v-model.number="formModel.ratedMW" label="Rated Power (MW)" type="number" outlined dense />
              </v-col>
              <v-col cols="12" sm="6">
                <v-text-field v-model.number="formModel.efficiency" label="Efficiency (%)" type="number" outlined dense />
              </v-col>
              <v-col cols="12" sm="6">
                <v-select v-model="formModel.status" :items="statusOptions" item-text="text" item-value="value" label="Status" outlined dense />
              </v-col>
              <v-col cols="12" sm="6">
                <v-text-field v-model.number="formModel.voltageKV" label="Voltage Level (kV)" type="number" outlined dense />
              </v-col>
              <v-col cols="12" sm="6">
                <v-text-field v-model.number="formModel.frequencyHz" label="Frequency (Hz)" type="number" outlined dense />
              </v-col>
              <v-col cols="12">
                <v-textarea v-model="formModel.remark" label="Remark" auto-grow outlined dense />
              </v-col>
            </v-row>
            <div class="caption grey--text mt-2">Connections: {{ selectedNode ? getNodeConnections(selectedNode.id).length : 0 }}</div>
          </v-card-text>
          <v-divider></v-divider>
          <div class="pa-2 d-flex justify-end">
            <v-btn text class="mr-2" @click="onCancelProp">Cancel</v-btn>
            <v-btn color="primary" dark @click="onConfirmProp">Confirm</v-btn>
          </div>
        </v-card>
      </div>

      <!-- Floating Assistant (bottom-right) -->
      <div class="ai-assistant">
        <!-- Docked (collapsed) bar -->
        <div v-if="!assistantExpanded" class="ai-dock elevation-6">
          <div class="dock-left">AI Assistant</div>
          <div class="dock-actions">
            <v-btn small icon @click="assistantExpanded=true">
              <v-icon>mdi-plus</v-icon>
            </v-btn>
          </div>
        </div>

        <!-- Expanded panel -->
        <v-card v-else class="ai-panel elevation-10">
          <div class="ai-header d-flex align-center justify-space-between">
            <div class="font-weight-bold">AI Assistant</div>
            <div>
              <v-btn small depressed class="mr-2" @click="assistantMode='text'">Text</v-btn>
              <v-btn small icon :color="isRecording ? 'red' : 'success'" class="mr-2" @click="toggleRecording">
                <v-icon>{{ isRecording ? 'mdi-microphone' : 'mdi-microphone-outline' }}</v-icon>
              </v-btn>
              <v-btn small icon @click="assistantExpanded=false"><v-icon>mdi-minus</v-icon></v-btn>
            </div>
          </div>
          <v-divider></v-divider>
          <div class="ai-body">
            <div class="ai-greeting">
              {{ assistantMode==='voice' && lastTranscript ? lastTranscript : 'Hello! I can help you manage node properties. Ask me anything about the canvas.' }}
            </div>

            <div v-if="assistantMode==='voice'" class="ai-voice text-center">
              <v-btn :color="isRecording ? 'red' : 'success'" class="mic-btn" fab large @click="toggleRecording">
                <v-icon large>mdi-microphone</v-icon>
              </v-btn>
              <div class="mt-2 grey--text text--darken-1">{{ isRecording ? 'Recording...' : 'Press to start recording' }}</div>
            </div>

            <div v-else class="ai-text">
              <v-text-field v-model="assistantText" outlined hide-details placeholder="Type your question..."/>
              <div class="mt-2 d-flex justify-end">
                <v-btn color="primary" @click="sendAssistantText">Send</v-btn>
              </div>
            </div>
          </div>
        </v-card>
      </div>
      <!-- Toast -->
      <v-snackbar v-model="snackbar.visible" :timeout="2500" :color="snackbar.color" top right>
        {{ snackbar.text }}
        <template v-slot:action="{ attrs }">
          <v-btn text v-bind="attrs" @click="snackbar.visible = false">Close</v-btn>
        </template>
      </v-snackbar>
    </v-row>
    <!-- Top save/load status bar -->
    <div class="pa-2 d-flex align-center" ref="statusBar">
      <span v-if="graphError" class="error--text caption">{{ graphError }}</span>
      <span v-else-if="saveMessage" class="caption grey--text">{{ saveMessage }}</span>
    </div>
  </div>
</template>

<script>
import { getDiagramGraph, saveDiagramGraph, listDiagrams } from '@/api/diagram.js'
let nextId = 1

export default {
  name: 'PowerCanvas',
  data() {
    return {
      canvasWidth: 1000,
      canvasHeight: 560,
      nodeRadius: 22,
      nodeWidth: 160,
      nodeHeight: 56,
      // Zoom and pan state
      zoom: 1,
      panX: 0,
      panY: 0,
      isPanning: false,
      panStart: { x: 0, y: 0 },
      nodes: [],
      edges: [],
      draggingNodeId: null,
      pendingDragNodeId: null,
      mouseDownAt: null,
      dragThreshold: 3,
      dragOffset: { x: 0, y: 0 },
      hoverNodeId: null,
      // Connection state
      connecting: false,
      connectSourceId: null,
      tempEdge: null,
      // Selected node/edge
      selectedNodeId: null,
      selectedEdgeId: null,
      propDialog: false,
      formModel: null,
      // Left palette
      paletteGroups: [
        {
          title: '⚡ Grid Layer',
          items: [
            { type: 'Transformer', label: 'Transformer', desc: 'Transmission equipment', bg: '#e8f0fe', icon: `<span>🔌</span>` },
            { type: 'Substation', label: 'Substation', desc: 'Distribution substation', bg: '#e8fff3', icon: `<span>🏭</span>` },
            { type: 'FeederLine', label: 'Feeder Line', desc: 'Distribution feeder', bg: '#fbe9e7', icon: `<span>📡</span>` },
            { type: 'Bus', label: 'Bus', desc: 'Busbar', bg: '#f3fff6', icon: `<span>➖</span>` }
          ]
        },
        {
          title: '🔌 Generators',
          items: [
            { type: 'Hydro', label: 'Hydropower', desc: '800 MW', bg: '#e3f2fd', icon: `<span>💧</span>` },
            { type: 'Wind', label: 'Wind Power', desc: '300 MW', bg: '#e8f5e9', icon: `<span>🌬️</span>` },
            { type: 'Thermal', label: 'Thermal Power', desc: '600 MW', bg: '#fff3e0', icon: `<span>🔥</span>` },
            { type: 'Nuclear', label: 'Nuclear Power', desc: '1000 MW', bg: '#ede7f6', icon: `<span>☢️</span>` },
            { type: 'Gas', label: 'Gas Power', desc: '500 MW', bg: '#fff8e1', icon: `<span>⛽</span>` },
            { type: 'Solar', label: 'Solar Power', desc: '200 MW', bg: '#fffde7', icon: `<span>☀️</span>` },
            { type: 'Biomass', label: 'Biomass Power', desc: '150 MW', bg: '#f1f8e9', icon: `<span>🌿</span>` },
            { type: 'Geothermal', label: 'Geothermal Power', desc: '100 MW', bg: '#f9fbe7', icon: `<span>🌋</span>` }
          ]
        },
        {
          title: '🔋 Storage',
          items: [
            { type: 'Battery', label: 'Battery Storage', desc: '100 MW', bg: '#e8eaf6', icon: `<span>🔋</span>` },
            { type: 'PumpedHydroStorage', label: 'Pumped Hydro Storage', desc: '500 MW', bg: '#e0f7fa', icon: `<span>🏞️</span>` },
            { type: 'FlywheelStorage', label: 'Flywheel Storage', desc: '20 MW', bg: '#f3e5f5', icon: `<span>🌀</span>` },
            { type: 'CompressedAirEnergyStorage', label: 'Compressed Air Energy Storage', desc: '50 MW', bg: '#e0f2f1', icon: `<span>💨</span>` }
          ]
        }
      ],
      typeOptions: ['Transformer','Substation','FeederLine','Bus','Hydro','Wind','Thermal','Nuclear','Gas','Solar','Biomass','Geothermal','Battery','PumpedHydroStorage','FlywheelStorage','CompressedAirEnergyStorage','Load','Generator'],
      statusOptions: [
        { text: 'Running', value: 'running' },
        { text: 'Standby', value: 'standby' },
        { text: 'Stopped', value: 'stopped' }
      ],
      // Assistant state (voice/text) from GasFired page concept
      assistantExpanded: false,
      assistantMode: 'text',
      isRecording: false,
      isConnected: false,
      assistantText: '',
      lastTranscript: '',
      wsUrl: process.env.VUE_APP_STT_WS || 'ws://localhost:8080/ws',
      wsPushUrl: process.env.VUE_APP_PUSH_WS || 'ws://localhost:8080/ws-push',
      sessionId: 'session-power-canvas',
      recognizer: null,
      // drawer
      propDrawer: false,
      appBarH: 64,
      // push socket
      pushWs: null,
      pushReconnectTimer: null,
      pushShouldReconnect: true,
      pushHeartbeatTimer: null,
      pushHeartbeatIntervalMs: 15000,
      // graph loading
      diagramId: null,
      loadingGraph: false,
      graphError: null,
      savingGraph: false,
      saveMessage: '',
      // diagrams list
      diagramOptions: [],
      loadingDiagrams: false
      ,
      // toast
      snackbar: { visible: false, color: 'success', text: '' },
      // history for undo
      history: [],
      historyIndex: -1,
      movedDuringDrag: false
    }
  },
  mounted() {
    this.autoFitCanvas()
    // Read actual AppBar height (if exists)
    this.$nextTick(() => {
      const appBars = document.querySelectorAll('.v-app-bar')
      if (appBars && appBars.length > 0) {
        const rect = appBars[0].getBoundingClientRect()
        this.appBarH = Math.max(56, Math.floor(rect.height))
      }
    })
    // connect backend push channel with same sessionId
    this.connectPushSocket()
    window.addEventListener('resize', this.autoFitCanvas)
    window.addEventListener('keydown', this.onKeyDown)
    window.addEventListener('mouseup', this.onGlobalMouseUp)
    // Initialize history baseline
    this.pushHistory()
    // Load diagram from backend
    const route = this.$route || {}
    this.diagramId = (route.params && route.params.diagramId) || (route.query && route.query.diagramId) || 1
    this.fetchDiagramList().then(() => this.fetchAndRenderDiagramGraph())
  },
  beforeDestroy() {
    window.removeEventListener('resize', this.autoFitCanvas)
    window.removeEventListener('keydown', this.onKeyDown)
    window.removeEventListener('mouseup', this.onGlobalMouseUp)
    this.pushShouldReconnect = false
    if (this.pushReconnectTimer) clearTimeout(this.pushReconnectTimer)
    if (this.pushHeartbeatTimer) clearInterval(this.pushHeartbeatTimer)
    if (this.pushWs) {
      try { this.pushWs.close() } catch (e) { /* eslint-disable-next-line no-console */ console.warn('[PushWS] close failed', e) }
    }
  },
  methods: {
    canUndo() { return this.historyIndex > 0 },
    // Select node
    selectNode(node) { this.selectedNodeId = node.id },
    isSelected(node) { return this.selectedNodeId === node.id },
    autoFitCanvas() {
      const el = this.$refs.canvasWrap
      if (!el) return
      // Make SVG width adapt to container, height fill viewport minus top padding and margins
      const rect = el.getBoundingClientRect()
      const top = rect.top
      const vh = window.innerHeight
      const statusEl = this.$refs.statusBar
      const statusH = statusEl ? Math.ceil(statusEl.getBoundingClientRect().height) : 0
      this.canvasWidth = Math.floor(rect.width)
      this.canvasHeight = Math.max(520, Math.floor(vh - top - statusH - 16))
    },
    addNode(type, x, y, label) {
      const id = `N${nextId++}`
      const node = {
        id,
        label: label ? String(label) : `${type}`,
        type,
        x: x != null ? x : this.canvasWidth / 2 + Math.round((Math.random() - 0.5) * 120),
        y: y != null ? y : this.canvasHeight / 2 + Math.round((Math.random() - 0.5) * 80),
        ratedMW: 0,
        efficiency: 100,
        status: 'running',
        voltageKV: 110,
        frequencyHz: 50,
        remark: ''
      }
      this.nodes.push(node)
    },
    getNodeFill(node) {
      if (node.type === 'Bus') return '#26a69a'
      if (node.type === 'Generator') return '#5c6bc0'
      if (node.type === 'Load') return '#ef5350'
      if (node.type === 'Substation') return '#ffb74d'
      if (node.type === 'Transformer') return '#90caf9'
      if (node.type === 'FeederLine') return '#80cbc4'
      if (['Hydro','Wind','Thermal','Nuclear','Gas','Solar','Biomass','Geothermal'].includes(node.type)) return '#5c6bc0'
      if (['Battery','PumpedHydroStorage','FlywheelStorage','CompressedAirEnergyStorage'].includes(node.type)) return '#8e99f3'
      return '#78909c'
    },
    getNodeIcon(node) {
      if (node.type === 'Substation') return '🏭'
      if (node.type === 'Bus') return '➖'
      if (node.type === 'Generator') return '⚡'
      if (node.type === 'Load') return '💡'
      if (node.type === 'Transformer') return '🔌'
      if (node.type === 'FeederLine') return '📡'
      if (node.type === 'Hydro') return '💧'
      if (node.type === 'Wind') return '🌬️'
      if (node.type === 'Thermal') return '🔥'
      if (node.type === 'Nuclear') return '☢️'
      if (node.type === 'Gas') return '⛽'
      if (node.type === 'Solar') return '☀️'
      if (node.type === 'Biomass') return '🌿'
      if (node.type === 'Geothermal') return '🌋'
      if (node.type === 'Battery') return '🔋'
      if (node.type === 'PumpedHydroStorage') return '🏞️'
      if (node.type === 'FlywheelStorage') return '🌀'
      if (node.type === 'CompressedAirEnergyStorage') return '💨'
      return '⬤'
    },
    getNode(id) {
      return this.nodes.find(n => n.id === id) || { x: 0, y: 0 }
    },
    getNodeByLabel(label) {
      return this.nodes.find(n => (n.label || '').toLowerCase() === String(label).toLowerCase()) || null
    },
    findNodeRef(ref) {
      if (!ref) return null
      if (typeof ref === 'string') {
        return this.nodes.find(n => n.id === ref || (n.label || '').toLowerCase() === ref.toLowerCase()) || null
      }
      if (ref.id) return this.nodes.find(n => n.id === ref.id) || null
      if (ref.label) return this.getNodeByLabel(ref.label)
      return null
    },
    deleteNode(ref) {
      const node = this.findNodeRef(ref)
      if (!node) return false
      const id = node.id
      // remove edges linked to node
      this.edges = this.edges.filter(e => e.from !== id && e.to !== id)
      // remove node
      this.nodes = this.nodes.filter(n => n.id !== id)
      return true
    },
    edgeExists(a, b) {
      return this.edges.some(e => (e.from === a && e.to === b) || (e.from === b && e.to === a))
    },
    connectNodes(fromRef, toRef) {
      const a = this.findNodeRef(fromRef)
      const b = this.findNodeRef(toRef)
      if (!a || !b || a.id === b.id) return false
      if (this.edgeExists(a.id, b.id)) return true
      this.edges.push({ id: `E${a.id}-${b.id}-${Date.now()}`, from: a.id, to: b.id })
      return true
    },
    updateNode(ref, patch) {
      const idx = this.nodes.findIndex(n => (ref && typeof ref === 'string' ? (n.id === ref || (n.label || '').toLowerCase() === ref.toLowerCase()) : (ref.id ? n.id === ref.id : (ref.label ? (n.label || '').toLowerCase() === String(ref.label).toLowerCase() : false))))
      if (idx === -1) return false
      const to = { ...this.nodes[idx] }
      const allowed = ['label','type','x','y','ratedMW','efficiency','status','voltageKV','frequencyHz','remark']
      for (const k of allowed) {
        if (Object.prototype.hasOwnProperty.call(patch, k) && typeof patch[k] !== 'undefined') {
          to[k] = patch[k]
        }
      }
      this.$set(this.nodes, idx, to)
      return true
    },
    getNodeConnections(nodeId) {
      return this.edges.filter(e => e.from === nodeId || e.to === nodeId)
    },
    onNodeMouseDown(node, evt) {
      // Only left mouse button arms dragging; actual drag starts after small movement
      if (evt && evt.button !== 0) return
      // Select node on mouse down and clear edge selection
      this.selectedNodeId = node.id
      this.selectedEdgeId = null
      this.pendingDragNodeId = node.id
      const pt = this.getMousePoint(evt)
      this.mouseDownAt = { x: pt.x, y: pt.y }
      this.dragOffset = { x: node.x - pt.x, y: node.y - pt.y }
    },
    onNodeMouseUp(node) {
      // Finish connection
      if (this.connecting && this.connectSourceId && this.connectSourceId !== node.id) {
        this.edges.push({ id: `E${this.connectSourceId}-${node.id}-${Date.now()}`, from: this.connectSourceId, to: node.id })
        this.pushHistory()
      }
      this.connecting = false
      this.connectSourceId = null
      this.tempEdge = null
      // Stop any dragging on node mouseup as well
      if (this.draggingNodeId && this.movedDuringDrag) {
        this.pushHistory()
      }
      this.draggingNodeId = null
      this.pendingDragNodeId = null
      this.mouseDownAt = null
      this.movedDuringDrag = false
    },
    onConnectorMouseDown(node, evt) {
      // Only left mouse button starts edge dragging from connector handle
      if (evt && evt.button !== 0) return
      this.connecting = true
      this.connectSourceId = node.id
      const pt = this.getMousePoint(evt)
      this.tempEdge = { x1: node.x, y1: node.y, x2: pt.x, y2: pt.y }
    },
    onCanvasMouseDown(event) {
      // Enable panning with left, middle, or right button on empty canvas
      if (event.button === 2 || event.button === 1) {
        this.isPanning = true
        this.panStart = { x: event.clientX - this.panX, y: event.clientY - this.panY }
        return
      }
      if (event.button === 0) {
        this.isPanning = true
        this.panStart = { x: event.clientX - this.panX, y: event.clientY - this.panY }
      }

      // Click canvas to cancel connection
      this.connecting = false
      this.connectSourceId = null
      this.tempEdge = null
      this.selectedNodeId = null
      this.selectedEdgeId = null
    },
    onCanvasMouseMove(evt) {
      // Handle panning
      if (this.isPanning) {
        this.panX = evt.clientX - this.panStart.x
        this.panY = evt.clientY - this.panStart.y
        return
      }
      
      // If left button is not held, stop any dragging
      if (evt && typeof evt.buttons === 'number' && (evt.buttons & 1) === 0) {
        this.draggingNodeId = null
        this.pendingDragNodeId = null
        this.mouseDownAt = null
      }
      const pt = this.getMousePoint(evt)
      // Promote pending to active dragging after threshold
      if (!this.draggingNodeId && this.pendingDragNodeId && this.mouseDownAt) {
        const dx = Math.abs(pt.x - this.mouseDownAt.x)
        const dy = Math.abs(pt.y - this.mouseDownAt.y)
        if (dx >= this.dragThreshold || dy >= this.dragThreshold) {
          this.draggingNodeId = this.pendingDragNodeId
        }
      }
      if (this.draggingNodeId) {
        const node = this.nodes.find(n => n.id === this.draggingNodeId)
        if (!node) return
        node.x = pt.x + this.dragOffset.x
        node.y = pt.y + this.dragOffset.y
        this.movedDuringDrag = true
      } else if (this.connecting && this.connectSourceId) {
        const src = this.getNode(this.connectSourceId)
        const side = this.computeAnchorSide(src, pt)
        const start = this.getAnchorCoords(src, side)
        this.tempEdge = { x1: start.x, y1: start.y, x2: pt.x, y2: pt.y }
      }
    },
    onKeyDown(e) {
      // Press Esc to cancel selection and connection state
      if (e.key === 'Escape') {
        this.selectedNodeId = null
        this.selectedEdgeId = null
        this.connecting = false
        this.connectSourceId = null
        this.tempEdge = null
      }
      // Delete key removes selection
      if (e.key === 'Delete' || e.key === 'Backspace') {
        this.onDelete()
      }
      // Ctrl+Z undo
      if ((e.ctrlKey || e.metaKey) && (e.key === 'z' || e.key === 'Z')) {
        this.undo()
      }
    },
    onCanvasMouseUp() {
      // Stop panning
      this.isPanning = false
      
      this.draggingNodeId = null
      this.pendingDragNodeId = null
      this.mouseDownAt = null
      // Close connection preview when drag/drop ends
      if (this.connecting && !this.connectSourceId) {
        this.tempEdge = null
        this.connecting = false
      }
    },
    onGlobalMouseUp() {
      // Ensure drag ends even if mouseup occurs outside SVG area
      this.draggingNodeId = null
      this.pendingDragNodeId = null
      this.mouseDownAt = null
    },
    getMousePoint(evt) {
      const svgEl = this.$refs.canvasWrap.querySelector('svg')
      const rect = svgEl.getBoundingClientRect()
      // Adjust for zoom and pan
      const x = (evt.clientX - rect.left - this.panX) / this.zoom
      const y = (evt.clientY - rect.top - this.panY) / this.zoom
      return { x, y }
    },
    onPaletteDragStart(e, payload) {
      if (!e || !e.dataTransfer) return
      try {
        const data = typeof payload === 'string' ? { type: payload } : payload
        e.dataTransfer.setData('application/json', JSON.stringify(data))
      } catch (err) {
        e.dataTransfer.setData('text/plain', String(payload && payload.type ? payload.type : payload))
      }
      // Optional: set drag image
      // const img = new Image(); img.src = 'data:image/svg+xml;base64,...'; e.dataTransfer.setDragImage(img, 10, 10)
    },
    onCanvasDrop(e) {
      let meta = null
      const json = e.dataTransfer.getData('application/json')
      if (json) {
        try { meta = JSON.parse(json) } catch (err) { meta = null }
      }
      const type = meta && meta.type ? meta.type : e.dataTransfer.getData('text/plain')
      if (!type) return
      const rect = this.$refs.canvasWrap.querySelector('svg').getBoundingClientRect()
      const x = e.clientX - rect.left
      const y = e.clientY - rect.top
      const label = meta && meta.label ? meta.label : undefined
      this.addNode(type, x, y, label)
      this.pushHistory()
      // Stop following mouse after drop
      this.draggingNodeId = null
      this.connecting = false
      this.connectSourceId = null
      this.tempEdge = null
    },
    // ===== Edge rendering (orthogonal with labels) =====
    computeAnchorSide(from, to) {
      const dx = to.x - from.x
      const dy = to.y - from.y
      if (Math.abs(dx) >= Math.abs(dy)) {
        return dx >= 0 ? 'right' : 'left'
      }
      return dy >= 0 ? 'bottom' : 'top'
    },
    getAnchorCoords(node, side) {
      const padX = this.nodeWidth / 2 + 2
      const padY = this.nodeHeight / 2 + 2
      if (side === 'left') return { x: node.x - padX, y: node.y }
      if (side === 'right') return { x: node.x + padX, y: node.y }
      if (side === 'top') return { x: node.x, y: node.y - padY }
      // bottom
      return { x: node.x, y: node.y + padY }
    },
    buildOrthPath(start, startSide, end, endSide) {
      const isStartHorizontal = (startSide === 'left' || startSide === 'right')
      const isEndHorizontal = (endSide === 'left' || endSide === 'right')
      // Same orientation on both ends → use a midpoint line perpendicular to the orientation
      if (isStartHorizontal && isEndHorizontal) {
        const midX = Math.round((start.x + end.x) / 2)
        return `M ${start.x} ${start.y} L ${midX} ${start.y} L ${midX} ${end.y} L ${end.x} ${end.y}`
      }
      if (!isStartHorizontal && !isEndHorizontal) {
        const midY = Math.round((start.y + end.y) / 2)
        return `M ${start.x} ${start.y} L ${start.x} ${midY} L ${end.x} ${midY} L ${end.x} ${end.y}`
      }
      // Mixed orientations → simple L-shape
      if (isStartHorizontal && !isEndHorizontal) {
        return `M ${start.x} ${start.y} L ${end.x} ${start.y} L ${end.x} ${end.y}`
      }
      // start vertical, end horizontal
      return `M ${start.x} ${start.y} L ${start.x} ${end.y} L ${end.x} ${end.y}`
    },
    getElbowPath(x1, y1, x2, y2) {
      const midX = Math.round((x1 + x2) / 2)
      return `M ${x1} ${y1} L ${midX} ${y1} L ${midX} ${y2} L ${x2} ${y2}`
    },
    getEdgePath(edge) {
      const fromNode = this.getNode(edge.from)
      const toNode = this.getNode(edge.to)
      const startSide = this.computeAnchorSide(fromNode, toNode)
      const endSide = this.computeAnchorSide(toNode, fromNode)
      const start = this.getAnchorCoords(fromNode, startSide)
      const end = this.getAnchorCoords(toNode, endSide)
      return this.buildOrthPath(start, startSide, end, endSide)
    },
    getEdgeLabelPos(edge) {
      const a = this.getNode(edge.from)
      const b = this.getNode(edge.to)
      const x = Math.round((a.x + b.x) / 2)
      const y = Math.round((a.y + b.y) / 2) - 6
      return { x, y }
    },
    isEdgeSelected(edge) {
      return this.selectedEdgeId === edge.id
    },
    formatEdgeLabel(edge) {
      const src = this.getNode(edge.from)
      const val = (typeof edge.powerMW === 'number') ? edge.powerMW : (typeof src.ratedMW === 'number' ? src.ratedMW : null)
      return val != null ? `P: ${Number(val).toFixed(0)} MW` : ''
    },
    onEdgeClick(edge) {
      this.selectedEdgeId = edge.id
      this.selectedNodeId = null
    },
    onDelete() {
      if (this.selectedEdgeId) {
        const id = this.selectedEdgeId
        const before = this.edges.length
        this.edges = this.edges.filter(e => e.id !== id)
        if (this.edges.length !== before) {
          this.pushHistory()
          this.showToast('Edge deleted', 'success')
        }
        this.selectedEdgeId = null
        return
      }
      if (this.selectedNodeId) {
        const ok = this.deleteNode({ id: this.selectedNodeId })
        if (ok) {
          this.pushHistory()
          this.showToast('Node deleted', 'success')
        }
        this.selectedNodeId = null
      }
    },
    // ===== History (Undo) =====
    pushHistory() {
      const snapshot = {
        nodes: this.nodes.map(n => ({ ...n })),
        edges: this.edges.map(e => ({ ...e }))
      }
      if (this.historyIndex < this.history.length - 1) {
        this.history = this.history.slice(0, this.historyIndex + 1)
      }
      this.history.push(snapshot)
      if (this.history.length > 50) {
        this.history.shift()
      }
      this.historyIndex = this.history.length - 1
    },
    undo() {
      if (!this.canUndo()) return
      this.historyIndex = Math.max(0, this.historyIndex - 1)
      const snap = this.history[this.historyIndex]
      if (snap) {
        this.nodes = snap.nodes.map(n => ({ ...n }))
        this.edges = snap.edges.map(e => ({ ...e }))
      }
    },
    openProperties(node) {
      // Ensure no dragging/connecting before opening properties
      this.draggingNodeId = null
      this.connecting = false
      this.connectSourceId = null
      this.tempEdge = null
      this.selectedNodeId = node.id
      this.formModel = { ...node }
      this.propDrawer = true
    },
    onCancelProp() {
      this.propDrawer = false
      this.formModel = null
    },
    onConfirmProp() {
      if (!this.selectedNodeId || !this.formModel) return
      const idx = this.nodes.findIndex(n => n.id === this.selectedNodeId)
      if (idx !== -1) {
        this.$set(this.nodes, idx, { ...this.nodes[idx], ...this.formModel })
      }
      this.onCancelProp()
    },
    resetCanvas() {
      this.nodes = []
      this.edges = []
      nextId = 1
    },
    // ===== Assistant (voice/text) =====
    async toggleRecording() {
      this.assistantMode = 'voice'
      if (this.isRecording) {
        this.stopRecognition()
      } else {
        await this.startRecognition()
      }
    },
    async startRecognition() {
      try {
        this.lastTranscript = ''
        const SpeechRecognizer = (await import('@/api/simpson-stt.js')).default
        this.recognizer = new SpeechRecognizer()
        await this.recognizer.recognize(
          this.wsUrl,
          window.localStorage.getItem('token'),
          this.handleRecognitionResult
        )
        this.isConnected = this.recognizer.isConnected
        this.isRecording = this.recognizer.isRecording
      } catch (error) {
        // eslint-disable-next-line no-console
        console.error('Start STT failed:', error)
        this.isConnected = false
        this.isRecording = false
      }
    },
    stopRecognition() {
      try {
        if (this.recognizer) this.recognizer.stop()
      } catch (error) {
        // eslint-disable-next-line no-console
        console.error('Stop STT failed:', error)
      } finally {
        this.isConnected = false
        this.isRecording = false
        this.recognizer = null
      }
    },
    handleRecognitionResult(data) {
      let raw = ''
      if (typeof data === 'string') {
        raw = data
      } else if (data && typeof data === 'object') {
        raw = data.text || data.message || data.content || ''
      }
      this.lastTranscript = this.sanitizeMessage(raw)
    },
    sendAssistantText() {
      const text = (this.assistantText || '').trim()
      if (!text) return
      const ws = this.pushWs
      if (!ws || ws.readyState !== WebSocket.OPEN) {
        this.showToast('Push channel not connected', 'error')
        return
      }
      try {
        ws.send(text)
        this.showToast('Sent', 'success')
        this.assistantText = ''
      } catch (e) {
        // eslint-disable-next-line no-console
        console.error('Send assistant text failed:', e)
        this.showToast('Send failed', 'error')
      }
    },
    // backend push using same sessionId
    connectPushSocket() {
      try {
        const clientId = window.localStorage.getItem('token')
        const url = `${this.wsPushUrl}?clientId=${encodeURIComponent(clientId)}`
        const ws = new WebSocket(url)
        this.pushWs = ws
        ws.onopen = () => {
          // eslint-disable-next-line no-console
          console.log('[PushWS] connected')
          this.startPushHeartbeat()
        }
        ws.onmessage = (evt) => {
          let payload = evt.data
          try { payload = JSON.parse(evt.data) } catch (e) { /* eslint-disable-line no-empty */ }
          // New envelope support: {"type":"command","result":{...}}; ignore chat
          if (payload && typeof payload === 'object' && Object.prototype.hasOwnProperty.call(payload, 'type')) {
            const t = String(payload.type || '').toLowerCase()
            if (t === 'chat') {
              return
            }
            if (t === 'command') {
              const inner = (payload.result && typeof payload.result === 'object')
                ? payload.result
                : (payload.data && typeof payload.data === 'object')
                  ? payload.data
                  : null
              if (inner) {
                payload = inner
              } else {
                // If no structured result for command, ignore
                return
              }
            }
          }
          // 1) Minimal format: {"add":"generator01"}
          if (payload && typeof payload === 'object' && payload.add) {
            this.addNode('Generator', undefined, undefined, payload.add)
            return
          }
          // 2) Full format: {"cmd":"add","node":{"type":"Generator","label":"generator01","x":640,"y":360}}
          if (payload && payload.cmd === 'add' && payload.node) {
            const n = payload.node || {}
            const type = n.type || 'Generator'
            const label = n.label || String(type)
            const x = typeof n.x === 'number' ? n.x : undefined
            const y = typeof n.y === 'number' ? n.y : undefined
            this.addNode(type, x, y, label)
            return
          }
          // 3) Delete: {"cmd":"delete","node":{"id":"N3"}} or {"cmd":"delete","node":{"label":"generator01"}}
          if (payload && payload.cmd === 'delete' && payload.node) {
            this.deleteNode(payload.node)
            return
          }
          // 4) Connect: {"cmd":"connect","from":{"id":"N1"},"to":{"label":"generator01"}}
          if (payload && payload.cmd === 'connect' && payload.from && payload.to) {
            this.connectNodes(payload.from, payload.to)
            return
          }
          // 5) Update: {"cmd":"update","node":{"id":"N2"},"patch":{"label":"G-02","x":420,"y":260}}
          if (payload && payload.cmd === 'update' && (payload.node || payload.id || payload.label)) {
            const ref = payload.node || payload.id || payload.label
            const patch = payload.patch || {}
            this.updateNode(ref, patch)
            return
          }
          // Fallback: notify unrecognized command/message
          const unknownMsg = (payload && typeof payload === 'object')
            ? (payload.cmd ? `Unrecognized command: ${payload.cmd}` : 'Unrecognized message')
            : `Unrecognized message: ${String(payload)}`
          this.showToast(unknownMsg, 'warning')
        }
        ws.onerror = (err) => {
          // eslint-disable-next-line no-console
          console.error('[PushWS] error', err)
        }
        ws.onclose = () => {
          // eslint-disable-next-line no-console
          console.log('[PushWS] closed')
          this.stopPushHeartbeat()
          if (this.pushShouldReconnect) {
            this.pushReconnectTimer = setTimeout(() => this.connectPushSocket(), 2000)
          }
        }
      } catch (e) {
        // eslint-disable-next-line no-console
        console.error('[PushWS] failed to connect', e)
      }
    }
    ,
    startPushHeartbeat() {
      if (this.pushHeartbeatTimer) clearInterval(this.pushHeartbeatTimer)
      this.pushHeartbeatTimer = setInterval(() => {
        try {
          const ws = this.pushWs
          if (ws && ws.readyState === WebSocket.OPEN) {
            ws.send('ping')
          }
        } catch (e) {
          // eslint-disable-next-line no-console
          console.warn('[PushWS] heartbeat send failed', e)
        }
      }, this.pushHeartbeatIntervalMs)
    }
    ,
    stopPushHeartbeat() {
      if (this.pushHeartbeatTimer) {
        clearInterval(this.pushHeartbeatTimer)
        this.pushHeartbeatTimer = null
      }
    }
    ,
    // ===== Graph loading from backend =====
    async fetchAndRenderDiagramGraph() {
      if (!this.diagramId) return
      this.loadingGraph = true
      this.graphError = null
      try {
        const payload = await getDiagramGraph(this.diagramId)
        this.loadFromBackend(payload)
      } catch (e) {
        // eslint-disable-next-line no-console
        console.error('Load diagram graph failed:', e)
        const msg = this.sanitizeMessage((e && e.message) || 'Load failed')
        this.graphError = msg
        this.showToast(`Load failed: ${msg}`, 'error')
      } finally {
        this.loadingGraph = false
      }
    },
    async fetchDiagramList() {
      this.loadingDiagrams = true
      try {
        const res = await listDiagrams({ pageNum: 1, pageSize: 50 })
        const list = (res && res.list) || []
        this.diagramOptions = list.map(it => ({ id: it.id, name: it.name }))
        if (!this.diagramId && list.length > 0) {
          // If route parameter is missing, default to first diagram
          this.diagramId = list[0].id
        }
      } catch (e) {
        // eslint-disable-next-line no-console
        console.error('List diagrams failed:', e)
      } finally {
        this.loadingDiagrams = false
      }
    },
    onDiagramChange() {
      this.fetchAndRenderDiagramGraph()
    },
    loadFromBackend(payload) {
      if (!payload) return
      const nodes = (payload.nodes || []).map(n => ({
        id: String(n.feId || n.id),
        label: n.label || '',
        type: n.type,
        x: Number((n.position && n.position.x) != null ? n.position.x : 0),
        y: Number((n.position && n.position.y) != null ? n.position.y : 0),
        ratedMW: Number((n.props && n.props.ratedMw) != null ? n.props.ratedMw : 0),
        efficiency: Number((n.props && n.props.efficiency) != null ? n.props.efficiency : 100),
        status: (n.props && n.props.status) || 'running',
        voltageKV: Number((n.props && n.props.voltageKv) != null ? n.props.voltageKv : 110),
        frequencyHz: Number((n.props && n.props.frequencyHz) != null ? n.props.frequencyHz : 50),
        remark: (n.props && (n.props.remark != null ? n.props.remark : ''))
      }))
      const edges = (payload.edges || []).map(e => ({
        id: String(e.id != null ? e.id : `E${e.source}-${e.target}`),
        from: String(e.source),
        to: String(e.target)
      }))
      this.nodes = nodes
      this.edges = edges
      this.pushHistory()
      const maxN = nodes.reduce((max, n) => {
        const m = /^N(\d+)$/.exec(n.id)
        return m ? Math.max(max, Number(m[1])) : max
      }, 0)
      if (maxN >= 1) { nextId = maxN + 1 }
    },
    // ===== Save to backend =====
    async saveCurrentGraph() {
      if (!this.diagramId) return
      this.savingGraph = true
      this.saveMessage = ''
      try {
        const payload = this.buildBackendPayload()
        await saveDiagramGraph(this.diagramId, payload)
        this.saveMessage = 'Saved'
        this.showToast('Saved successfully', 'success')
      } catch (e) {
        // eslint-disable-next-line no-console
        console.error('Save diagram graph failed:', e)
        const msg = this.sanitizeMessage((e && e.message) || 'Save failed')
        this.graphError = msg
        this.showToast(`Save failed: ${msg}`, 'error')
      } finally {
        this.savingGraph = false
        setTimeout(() => { this.saveMessage = '' }, 2000)
      }
    },
    buildBackendPayload() {
      const nodes = this.nodes.map(n => ({
        feId: n.id,
        label: n.label,
        type: n.type,
        x: Math.round(Number(n.x != null ? n.x : 0)),
        y: Math.round(Number(n.y != null ? n.y : 0)),
        ratedMw: Number(n.ratedMW != null ? n.ratedMW : 0),
        efficiency: Number(n.efficiency != null ? n.efficiency : 100),
        status: n.status || 'running',
        voltageKv: Number(n.voltageKV != null ? n.voltageKV : 110),
        frequencyHz: Number(n.frequencyHz != null ? n.frequencyHz : 50),
        remark: (n.remark != null && n.remark !== '') ? n.remark : null
      }))
      const edges = this.edges.map(e => ({
        source: String(e.from),
        target: String(e.to)
      }))
      return { nodes, edges }
    }
    ,
    // ===== Zoom and Pan =====
    zoomIn() {
      this.zoom = Math.min(this.zoom * 1.2, 3)
    },
    zoomOut() {
      this.zoom = Math.max(this.zoom / 1.2, 0.1)
    },
    resetZoom() {
      this.zoom = 1
      this.panX = 0
      this.panY = 0
    },
    onWheel(event) {
      event.preventDefault()
      const delta = event.deltaY > 0 ? 0.9 : 1.1
      const newZoom = Math.max(0.1, Math.min(3, this.zoom * delta))
      
      // Get mouse position relative to SVG
      const rect = event.currentTarget.getBoundingClientRect()
      const mouseX = event.clientX - rect.left
      const mouseY = event.clientY - rect.top
      
      // Calculate zoom center
      const zoomCenterX = (mouseX - this.panX) / this.zoom
      const zoomCenterY = (mouseY - this.panY) / this.zoom
      
      // Update pan to keep zoom center
      this.panX = mouseX - zoomCenterX * newZoom
      this.panY = mouseY - zoomCenterY * newZoom
      
      this.zoom = newZoom
    },
    // ===== Utilities =====
    sanitizeMessage(msg) {
      if (msg == null) return ''
      const cleaned = String(msg).replace(/[\u4E00-\u9FFF\u3000-\u303F\uFF00-\uFFEF]/g, '').trim()
      return cleaned
    },
    showToast(text, color = 'success') {
      this.snackbar.text = this.sanitizeMessage(text)
      this.snackbar.color = color
      this.snackbar.visible = true
    }
  }
}
</script>

<style scoped>
.canvas-wrap {
  width: 100%;
  height: 560px;
  border: 1px dashed #cfd8dc;
  border-radius: 6px;
  background: #fafafa;
  overflow: hidden;
  user-select: none;
}
.canvas-wrap { position: relative; }
.grabbable { cursor: grab; }
.grabbing { cursor: grabbing; }
.canvas-toolbar {
  position: absolute;
  top: 8px;
  right: 8px;
  display: flex;
  align-items: center;
  z-index: 10;
}

.zoom-controls {
  position: absolute;
  top: 8px;
  left: 8px;
  display: flex;
  align-items: center;
  background: rgba(255, 255, 255, 0.9);
  border-radius: 8px;
  padding: 4px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  z-index: 10;
}

.zoom-btn {
  margin: 0 2px;
}

.zoom-level {
  font-size: 12px;
  color: #666;
  margin-left: 8px;
  padding: 0 8px;
  border-left: 1px solid #ddd;
  min-width: 40px;
  text-align: center;
}
.toolbar-select { width: 220px; }
.node { cursor: move; }
.connector-handle { cursor: crosshair; }
.palette { padding: 8px; }
.palette-group-title { font-size: 13px; color: #455a64; font-weight: 600; padding: 6px 12px; }
.palette-item { display: flex; align-items: center; padding: 10px 12px; border-bottom: 1px solid #eee; cursor: grab; }
.palette-item:last-child { border-bottom: none; }
.palette-item:hover { background: #fafafa; }
.prop-panel { position: fixed; right: 16px; width: 520px; z-index: 900; }
.prop-panel .v-card__text { max-height: calc(100vh - 220px); overflow-y: auto; }
.edge-label { pointer-events: none; user-select: none; }
.clickable-edge { cursor: pointer; }
.ai-assistant { position: fixed; right: 24px; bottom: 24px; z-index: 1000; }
.ai-dock { display: flex; align-items: center; background: #fff; border-radius: 14px; padding: 10px 12px; box-shadow: 0 6px 20px rgba(0,0,0,0.12); }
.dock-left { font-weight: 600; margin-right: 12px; }
.dock-actions { display: flex; align-items: center; }
.ai-panel { width: 360px; background: #fff; border-radius: 12px; overflow: hidden; }
.ai-header { padding: 10px 12px; }
.ai-body { padding: 14px; }
.ai-greeting { color: #5f6368; margin-bottom: 12px; }
.mic-btn { width: 84px; height: 84px; }
</style>


